import re
import sys

__all__ = ["keyGroups", "KeyGroupParserError", "KeyGroupParser"]

class RegularExpressionManager(dict):
    """
    This object acts as a container for all regular expressions. We subclass the
    dict object as the container can be associated by a dictionary, however, it
    needs to be modified to suit the specific needs of the singleton.
    """
    def __init__(self, *args, **kw):
        """
        Default constructor, initialise all variables and ensure that we compile
        all needed regular expressions here.
        """
        self.allowModification = True
        dict.__init__(self, *args, **kw)

        keyGroupExpression = re.compile(r"""
                \"(?P<name>[a-zA-Z0-9_.-]+?)\" # Match the name of the keygroup
                [\s]*                        # Any whitespace between keygroup
                                             #          name and opening brace
                {                            # Opening brace
                (?P<body>.*)                 # Match the body of the keygroup
                }                            # Match the final brace
                """, re.VERBOSE|re.DOTALL)

        self.__setitem__("keyGroupExpression", keyGroupExpression)

        keyExpression = re.compile(r"""
                \"(?P<name>[a-zA-Z0-9_^\n\"]+)\" # The key encased in speech marks
                [\s]*                            # Whitespace to split the keys
                \"(?P<val>[^\n]+)\"              # The val encased in speech marks
                """, re.VERBOSE)
        self.__setitem__("keyExpression", keyExpression)

        self.allowModification = False

    def __getattr__(self, item):
        """
        Executed when an unkown attribute is accessed, assume we want to access
        the value referenced by the item name.

        @param string item The name as to the object we wish to retrieve.
        """
        return self.__getitem__(item)

    def __setitem__(self, item, value):
        """
        Executed when we attempt to assign an item via the dictionary method.
        We overwrite this to remove the ability to assign new expressions from
        outside this object assignment.

        @param string item The name of the item
        @param mixed value The value of the key-value pair.
        """
        if self.allowModification:
            dict.__setitem__(self, item, value)

class KeyGroupParser(object):
    """
    This object will be a reference to a single keygroup from a file. This
    object parses then stores all information in Python dictioanries for ease
    of access. We also simulate dictionary assignment and calls so it's more
    Pythonic and easier to work with for people used to Python. Since parsing
    is done once, speed up over the usual KeyValues module should be phenominal.
    """
    def __init__(self, fileObject):
        """
        Default constructor, initialise variables and ensure that we are passed
        a valid stream object

        @param file fileObject An open stream for a file
        """
        if not isinstance(fileObject, file):
            raise ValueError("KeyGroupParser expects type 'file', but instead"
                             "received type '%s'" % type(file).__name__)
        self.rawData  = fileObject.readlines()
        self.rawData  = filter(lambda x: not x.startswith('//') and
                                x.strip() != "", self.rawData)
        self.rawData  = "".join(self.rawData)
        self.keyGroup = self.KeyGroup(self.rawData)

    def __getattr__(self, key):
        """
        Executed when an unknown attribute is attempting to be read. We can
        assume that it'll either be a sub-keygroup or key from the main keygroup
        and as that instance already handles this, we can return the value given
        from that.

        @param string key The name of the attribute which was called
        @return KeyGroup|Key The key group or key referenced by the name
        """
        return self.keyGroup.__getattr__(key)

    def __iter__(self):
        """
        Iterates through the main keygroup's sub-keygroups and keys

        @return iterator The main-keygroups iterator object
        """
        return self.keyGroup.__iter__()

    def __getitem__(self, key):
        """
        Executed when an unknown attribute is attempting to be read. We can
        assume that it'll either be a sub-keygroup or key from the main keygroup
        and as that instance already handles this, we can return the value given
        from that.

        @param string key The name of the attribute which was called
        @return KeyGroup|Key The key group or key referenced by the name
        """
        return self.keyGroup.__setattr__(key)

    def __str__(self):
        """
        Return a string representation of this instance, just return the string
        representation of the main encasing key group.
        """
        return self.keyGroup.__str__()

    def __setitem__(self, key, value):
        """
        This enables us to use the keygroup object as a dictionary object to
        support item assignment similar to key assignment. Since the main
        keygroup instance supports this, we will refer to that function.

        @param string key The name of the key-value pair
        @param mixed value The value of the key-value pair
        """
        self.keyGroup.__setitem__(key, value)

    def getKeyGroups(self):
        """
        Retrieve all the child keygroups of this parent.

        @return list Container of all child KeyGroup objects
        """
        return self.keyGroup.keyGroups.values()

    def getKeys(self):
        """
        Retrieve all the child keys of this parent.

        @return list Container of all child Key objects
        """
        return self.keyGroup.keys.values()

    def save(self, fileObject):
        """
        Saves the keygroup to another file object which must have a valid
        'write' function.

        @param file fileObject An open stream for a file in write mode
        """
        if not isinstance(fileObject, file):
            raise ValueError("KeyGroupParser expects type 'file', but instead"
                             "received type '%s" % type(file).__name__)
        fileObject.write(str(self.keyGroup))

    class KeyGroup(object):
        """
        This object is for every KeyGroup in the file. It must hold all the
        information for sub-keygroups and keyvalues.
        """
        def __init__(self, rawData):
            """
            Default constructor, initialise variables and check to ensure that
            the input provided is parsable.

            @param str rawData The raw string data of the keygroup
            """
            self.rawData   = rawData.strip()
            self.name      = None
            self.match     = expresions.keyGroupExpression.match(self.rawData)
            self.keys      = {}
            self.keyGroups = {}
            if self.match is None:
                if self.rawData[-1] != "}":
                    raise KeyGroupParserError(self, len(self.rawData),
                                        "No end brace found in keygroup")
                elif self.rawData[0] != '"':
                    raise KeyGroupParserError(self, 0, "Name is not encased"
                                                       "within speech marks")
                elif self.rawData.count("}") != self.rawData.count("{"):
                    raise KeyGroupParserError(self, 0, "Bracket count may not"
                                                       "be equal, count the"
                                                       "number of braces.")
                else:
                    raise KeyGroupParserError(self, 0, "An unkown error occured"
                                                       " whilst parsing. Check"
                                                       " the keygroup.")

            self.name = self.match.group("name")
            self.body = self.match.group("body")

            for nest in expresions.keyGroupExpression.finditer(self.body):
                """ We have found nested keygroups, split it and parse that """
                start, end = nest.span()
                self.nestedKeygroup = self.body[start:end]
                self.body = self.body.replace(self.nestedKeygroup, "")
                try:
                    keyGroup = KeyGroupParser.KeyGroup(self.nestedKeygroup)
                    self.keyGroups[keyGroup.name] = keyGroup
                except KeyGroupParserError:
                    """
                    Don't stop execution of the parser, just print the error and
                    carry on with the next keygroup
                    """
                    sys.exc_info()
                    continue

            for keyMatch in expresions.keyExpression.finditer(self.body):
                """
                Iterate through all found keys in the regex and append them to
                the dictionary object. We store all keys as KeyGroupParser.Key
                objects so they're easier to manipulate etc.
                """
                key   = keyMatch.group("name")
                value = keyMatch.group("val")
                self.keys[key] = KeyGroupParser.Key(key, value)


        def __str__(self):
            """
            Executed automatically when this object is attempting to be
            converted to a string object, return the raw data.

            @return str A string representation of this object
            """
            stringRepr = '"%s"\n{\n' % self.name

            for keygroup in self.keyGroups.itervalues():
                stringRepr += str(keygroup)

            for key in self.keys.itervalues():
                stringRepr += str(key)

            stringRepr += "}\n"
            return stringRepr

        def __getitem__(self, key):
            """
            Executed when we attempt to simulate retrieval of information from
            this object as a dictionary object.

            @param string key The key of the object that we wish to retrieve
            @return Key|KeyGroup The object which is referenced by key, KeyGroup
                                 takes presedence if there are name collisions.
            """
            if key in self.keyGroups:
                return self.keyGroups[key]

            if key in self.keys:
                return self.keys[key]

            """ No key has been found, raise a KeyError """
            raise KeyError("The KeyGroup or Key '%s' does not" % key +
                            "exist in KeyGroup %s" % self.name)

        def __getattr__(self, key):
            """
            This is executed when an unkown attribute is accessed. This has the
            same action as __getitem__ so we just call that.

            @param string key The name of the unrecognised attribute
            @return Key|KeyGroup The object which is referenced by key, KeyGroup
                                 takes presedence if there are name collisions.
            """
            return self.__getitem__(key)

        def __setitem__(self, key, value):
            """
            Simulates dictionary object assignment; e.g. thisObject['key'] = 1.
            This enables us to add a key via this method as it treats the key
            group object as a python dictionary.

            @param string key The name of the key-value pair
            @param mixed value The value of the object (converted to a string)
            """
            keyInstance = KeyGroupParser.Key(key, str(value))
            self.keys[key] = keyInstance

        def __repr__(self):
            """
            Return a string object which represents how python interacts with
            this object

            @return string Python representation simulation
            """
            return 'KeyGroup(%s)' % self.name

        def __iter__(self):
            """
            Iterate over all the sub-keygroups and keys within this keygroup.
            This will just simply join all the keygroups and keys to a single
            pool where we shall return the iterator of that pool for ease.

            @return iterator Object to iterate through the keygroup / key pool.
            """
            keyGroupPool = self.keyGroups.values()
            keyGroupPool.extend(self.keys.values())
            return keyGroupPool.__iter__()

        def getKeyGroups(self):
            """
            Retrieve all the child keygroups of this parent.

            @return list Container of all child KeyGroup objects
            """
            return self.keyGroups.values()

        def getKeys(self):
            """
            Retrieve all the child keys of this parent.

            @return list Container of all child Key objects
            """
            return self.keys.values()

    class Key(object):
        """
        This object simulates a key object. This should store all information
        regarding a single key-value pair.
        """
        def __init__(self, key, value):
            """
            Default constructor, intialise and store all variables.

            @param string key The key of the key-value pair
            @param string value The value of the key-value pair
            """
            self.key   = key
            self.value = value

        def __str__(self):
            """
            Executed automatically when this object is attempting to be
            converted to a string object, return the raw data.

            @return str A string representation of this object
            """
            return '"%s"\t"%s"\n' % (self.key, self.value)

        def __repr__(self):
            """
            Return a string object which represents how python interacts with
            this object

            @return string Python representation simulation
            """
            return "Key(\"'%s'='%s'\")" % (self.key, self.value)

class KeyGroupParserError(Exception):
    """
    This class is for a custom Exception created by the key group parser. It
    alters the constructor to allow for specific objects which we can use to
    build a default template for all exceptions.
    """
    def __init__(self, keyGroup, errorPosition, message):
        """
        Custom constructor, override Exception's constructor to specify explicit
        and unique paramters. Build a default template which all error messages
        adhere to to form unity.

        @param KeyGroup keyGroup The key group object where the error was raised
        @param integer errorPosition The index in the string which the error
                                                                       was found
        @param string message Any additional message to help fix the error
        """
        self.text = str(keyGroup)
        self.text = self.text[errorPosition - 5 : errorPosition + 5]
        Exception.__init__(self, "An error occured in the following keygroup "
                                  "around this position:\n%s\nPossible Error:\n"
                                  "%s" % (self.text, message))

class KeyGroupParserManager(object):
    """
    This object's sole purpose is to manage the saving of the instances of the
    parsed keygroups by holding them in a dictionary with the key of their file
    system path. For example, when we parse a keygroup from a given path, we
    shall store the instance created by the parser and reference it by the key
    (which is the path).
    """
    def __init__(self):
        """
        Default constructor, initialise variables and initialise everything
        needed.
        """
        self.keyGroups = {}

    def __getitem__(self, key):
        """
        This function simulates dictionary slicing and calling via the []
        operators. We can assume that a system file path was passed.

        @param string key The path to the keygroup
        @return KeyGroup
        """
        return self.keyGroups[key.replace('\\', '/')]

    def keyGroupLoad(self, filePath):
        """
        This function's purpose is to load the KeyGroup into memory and parse
        it from the file. If we have previously parsed it, we will return the
        instance.

        @param string filePath The system path to the keygroup
        """
        filePath = filePath.replace("\\", "/")
        if filePath not in self.keyGroups:
            fileStream = open(filePath, 'r')
            self.keyGroups[filePath] = KeyGroupParser(fileStream)
            fileStream.close()
        return self.keyGroups[filePath]


""" Global singletons initialised here """
expresions = RegularExpressionManager()
keyGroups  = KeyGroupParserManager()